SolidJSに入門してみた2:ForとIndexの違い
こんちには。
データアナリティクス事業本部機械学習チームの中村です。
SolidJSのチュートリアルやってみた記事その2です。今回は制御フローFor
とIndex
の違いについて書きます。
なお本記事は以下のチュートリアルをやってみて、ココは結構Reactと違うなとか、個人的にハマった部分などフォーカスしています。
詳しく知りたい方はぜひチュートリアルをやってみてください!
環境セットアップ
お手元で動かしたい場合は、以下でOKです。 Reactでもおなじみのグルグルするやつが起動します。
$ npx degit solidjs/templates/ts sample-solidjs $ cd sample-solidjs $ npm i $ npm run dev
リストサンプルを例に
前回も使いましたが、リスト表示するコンポーネントを題材にします。
制御フローFor
を使用した場合は、以下でした。
import type { Component } from 'solid-js'; import { createSignal } from 'solid-js'; import { JSX } from 'solid-js'; import { For } from 'solid-js'; export const SampleList: Component = () => { const [cats, setCats] = createSignal([ { id: 'J---aiyznGQ', name: 'Keyboard Cat' }, { id: 'z_AbfPXTKms', name: 'Maru' }, { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' } ]); const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => { setCats([ ...cats().slice(1, cats().length), cats()[0] ]); } return ( <> <button onClick={changeListHandler}>Click Me</button> <ul> <For each={cats()}>{ (cat, i) => { console.log(`rendered ${i()}: ${cat.name}`); return ( <li> {i()}: {cat.name} </li> ); }}</For> </ul> </> ); };
Indexを使った場合
For
と似たものとしてIndex
という制御フローもあります。これを使用して先ほどのサンプルを以下のように書き直すことができます。
import type { Component } from 'solid-js'; import { createSignal } from 'solid-js'; import { JSX } from 'solid-js'; import { Index } from 'solid-js'; export const SampleList: Component = () => { const [cats, setCats] = createSignal([ { id: 'J---aiyznGQ', name: 'Keyboard Cat' }, { id: 'z_AbfPXTKms', name: 'Maru' }, { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' } ]); const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => { setCats([ ...cats().slice(1, cats().length), cats()[0] ]); } return ( <> <button onClick={changeListHandler}>Click Me</button> <ul> <Index each={cats()}>{ (cat, i) => { console.log(`rendered ${i}: ${cat().name}`); return ( <li> {i}: {cat().name} </li> ); }}</Index> </ul> </> ); };
ほぼ違いはないのですが、Signalとなっているものが配列のインデックスi
ではなく要素cat
であるため、要素のアクセスにはcat()
を使用しています。
ForとIndexの違い
再レンダリングが発生する条件が異なり、Indexの方がより再レンダリングが発生しにくくなっています。
- For
- 現在の配列と異なる要素が入ってくると再レンダリングが発生
- 要素がプリミティブ(文字列・数値など)な場合、値が一致するなら異なる要素とはみなされない。
- 要素がオブジェクトの場合、値が一致しても異なるオブジェクトであれば異なる要素とみなされる。
- 異なる要素でない場合でも、要素数が増えた場合は再レンダリングが発生
- Index
- 要素数が増えた場合のみ再レンダリングが発生
確かめてみる
まずはFor
の例に戻り、ボタンを押すと新しいオブジェクトに置き換わる場合を見てみます。
import type { Component } from 'solid-js'; import { createSignal } from 'solid-js'; import { JSX } from 'solid-js'; import { For } from 'solid-js'; export const SampleList: Component = () => { const [cats, setCats] = createSignal([ { id: 'J---aiyznGQ', name: 'Keyboard Cat' }, { id: 'z_AbfPXTKms', name: 'Maru' }, { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' } ]); const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => { setCats([ ...cats().slice(1, cats().length), { id: '1000', name: 'New Cat1'} ]); } return ( <> <button onClick={changeListHandler}>Click Me</button> <ul> <For each={cats()}>{ (cat, i) => { console.log(`rendered ${i()}: ${cat.name}`); return ( <li> {i()}: {cat.name} </li> ); }}</For> </ul> </> ); };
上記の場合、ボタンを押す都度再レンダリングが発生します。これをIndex
に置き換えると、
import type { Component } from 'solid-js'; import { createSignal } from 'solid-js'; import { JSX } from 'solid-js'; import { Index } from 'solid-js'; export const SampleList: Component = () => { const [cats, setCats] = createSignal([ { id: 'J---aiyznGQ', name: 'Keyboard Cat' }, { id: 'z_AbfPXTKms', name: 'Maru' }, { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' } ]); const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => { setCats([ ...cats().slice(1, cats().length), { id: '1000', name: 'New Cat1'} ]); } return ( <> <button onClick={changeListHandler}>Click Me</button> <ul> <Index each={cats()}>{ (cat, i) => { console.log(`rendered ${i}: ${cat().name}`); return ( <li> {i}: {cat().name} </li> ); }}</Index> </ul> </> ); };
ボタンを押しても再レンダリングが発生しないことが分かります。(ちなみに表示にはきちんと変更が反映されます)
逆にIndex
のコールバック処理で以下のように文字列の連結などをしていると、
import type { Component } from 'solid-js'; import { createSignal } from 'solid-js'; import { JSX } from 'solid-js'; import { Index } from 'solid-js'; export const SampleList: Component = () => { const [cats, setCats] = createSignal([ { id: 'J---aiyznGQ', name: 'Keyboard Cat' }, { id: 'z_AbfPXTKms', name: 'Maru' }, { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' } ]); const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => { setCats([ ...cats().slice(1, cats().length), { id: '1000', name: 'New Cat1'} ]); } return ( <> <button onClick={changeListHandler}>Click Me</button> <ul> <Index each={cats()}>{ (cat, i) => { console.log(`rendered ${i}: ${cat().name}`); const name_with_id = `${cat().name} (id:${cat().id})` return ( <li> {i}: {name_with_id} </li> ); }}</Index> </ul> </> ); };
この場合、コールバックは実行されないため画面表示が更新されません。これをFor
で記述すると、
import type { Component } from 'solid-js'; import { createSignal } from 'solid-js'; import { JSX } from 'solid-js'; import { For } from 'solid-js'; export const SampleList: Component = () => { const [cats, setCats] = createSignal([ { id: 'J---aiyznGQ', name: 'Keyboard Cat' }, { id: 'z_AbfPXTKms', name: 'Maru' }, { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' } ]); const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => { setCats([ ...cats().slice(1, cats().length), { id: '1000', name: 'New Cat1'} ]); } return ( <> <button onClick={changeListHandler}>Click Me</button> <ul> <For each={cats()}>{ (cat, i) => { console.log(`rendered ${i()}: ${cat.name}`); const name_with_id = `${cat.name} (id:${cat.id})` return ( <li> {i()}: {name_with_id} </li> ); }}</For> </ul> </> ); };
表示がきちんと更新されることがわかります。
ただこの場合、コールバック内に関数を定義してあげればIndex
のまま実装することも可能です。
import type { Component } from 'solid-js'; import { createSignal } from 'solid-js'; import { JSX } from 'solid-js'; import { Index } from 'solid-js'; export const SampleList: Component = () => { const [cats, setCats] = createSignal([ { id: 'J---aiyznGQ', name: 'Keyboard Cat' }, { id: 'z_AbfPXTKms', name: 'Maru' }, { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' } ]); const changeListHandler: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = () => { setCats([ ...cats().slice(1, cats().length), { id: '1000', name: 'New Cat1'} ]); } return ( <> <button onClick={changeListHandler}>Click Me</button> <ul> <Index each={cats()}>{ (cat, i) => { console.log(`rendered ${i}: ${cat().name}`); const name_with_id = () => `${cat().name} (id:${cat().id})` return ( <li> {i}: {name_with_id()} </li> ); }}</Index> </ul> </> ); };
公式の説明によると
公式の説明では以下のように記載されています。
経験則では、プリミティブを扱うときには を使用します。
要は文字列・数値などのプリミティブな要素を持つ配列を扱う場合はIndex
を使用し、そうでない場合はFor
を使うことを推奨されています。
オブジェクトの配列であってもIndex
を使用することで、再レンダリングを抑制できますが、表示に影響がないか十分に注意して使用する必要がありそうです。逆によくわからない場合はFor
を使うことにより、表示上の問題が出にくくなるといえそうです。
まとめ
いかがでしたでしょうか?For
とIndex
の制御フローの違いがちょっと難しかったですので、サンプルを動かしながら再レンダリングの条件を色々調べて理解することができました。
この記事がSolidJSの理解の助けになりましたら幸いです。